﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SuperSocket.SocketEngine;
using System.Configuration;
using System.IO;
using System.Threading;
using System.Collections;
using Valentia.Kinection.DataTypes;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketEngine.Configuration;
using SuperWebSocket;

namespace Valentia.Kinection.Service
{
    internal class WebSocketServerWrapper
    {
        #region DATA MEMBERS
        
        /// <summary>
        /// Run secure socket server or simple socket server
        /// </summary>
        private bool runSecureSocketServer = true;

        /// <summary>
        /// Simple socket server
        /// </summary>
        private WebSocketServer socketServer;

        /// <summary>
        /// Secure socket server
        /// </summary>
        private WebSocketServer secureSocketServer;

        /// <summary>
        /// Collection of connected web socket clients
        /// </summary>
        private Dictionary<String, WebSocketClients> connectedWebSockets;

        /// <summary>
        /// Collection of connected and authenticated service clients
        /// </summary>
        private Dictionary<String, WebSocketClients> authenticatedWebSockets;
        
        #endregion

        #region PROPERTIES

        /// <summary>
        /// Socket server is running
        /// </summary>
        internal bool Started { get; private set; }

        /// <summary>
        /// Socket server is stopping
        /// </summary>
        internal bool Stopping { get; private set; }

        /// <summary>
        /// Collection of authenticated service cilents
        /// </summary>
        internal Hashtable AuthenticatedClients { get { lock (this.connectedWebSockets) { return new Hashtable(this.authenticatedWebSockets); } } }

        #endregion

        #region CONSTRUCTOR

        /// <summary>
        /// Constructor.
        /// </summary>
        internal WebSocketServerWrapper(KinectService serviceInstance)
        {
            this.connectedWebSockets = new Dictionary<String, WebSocketClients>();
            this.authenticatedWebSockets = new Dictionary<String, WebSocketClients>();

            this.Started = false;
            this.Stopping = false;
        }

        #endregion

        #region WEB SOCKET SERVER EVENT HANDLER METHODS

        /// <summary>
        /// Checks the connection already created from end point or exception occurred during 
        /// adding new session to collection it terminates session
        /// </summary>
        /// <param name="session">Web socket session</param>
        /// <param name="secured">Is secure connection</param>
        private void OnNewSession(WebSocketSession session, bool secured)
        {
            if (this.Stopping || !this.Started) return;

            String remoteEP = String.Empty;
            
            WebSocketClients wsClient = null;
            bool connected = false;
            
            try
            {
                Monitor.Enter(this.connectedWebSockets);

                remoteEP = session.RemoteEndPoint.ToString();

                this.Log("Client connected from: {0}", remoteEP);

                if (this.connectedWebSockets.ContainsKey(remoteEP))
                {
                    wsClient = this.connectedWebSockets[remoteEP];
                    wsClient.Close("Another client connected with same end point: " + remoteEP);
                }

                wsClient = new WebSocketClients(this, session, secured);
                this.connectedWebSockets.Add(remoteEP, wsClient);
                connected = true;
            }
            catch (Exception exp) { this.Log(exp, "Exception while client connected from: {0}. {1}", remoteEP, exp.Message); }
            finally
            {
                try
                {
                    Monitor.Exit(this.connectedWebSockets);
                }
                finally
                {
                    if (!connected && wsClient != null)
                    {
                        { try { this.connectedWebSockets.Remove(remoteEP); } catch { } }
                        { try { wsClient.Close("Adding client instance to collection failed at session start"); } catch { } }
                    }
                }
            }
        }

        /// <summary>
        /// Removes from collections and closes the client session
        /// </summary>
        /// <param name="session">Web socket session</param>
        /// <param name="closeReason">Session closing reason</param>
        /// <param name="secured">Is session secured</param>
        private void OnSessionClosed(WebSocketSession session, CloseReason closeReason, bool secured)
        {
            if (this.Stopping || !this.Started) return;

            String remoteEP = String.Empty;
            try
            {
                Monitor.Enter(this.connectedWebSockets);

                remoteEP = session.RemoteEndPoint.ToString();
                this.Log("Client disconnected from: {0}. Reason: {1}", remoteEP, closeReason.ToString());

                WebSocketClients wsClient = null;

                if (this.connectedWebSockets.ContainsKey(remoteEP))
                {
                    wsClient = this.connectedWebSockets[remoteEP];
                    this.connectedWebSockets.Remove(remoteEP);
                }

                if (wsClient == null) return;

                String wsClientUserName = (wsClient.UserName ?? String.Empty).Trim();
                if (this.authenticatedWebSockets.ContainsKey(wsClientUserName))
                {
                    this.authenticatedWebSockets.Remove(wsClientUserName);
                    if (wsClientUserName.Length > 0) { this.Log("Disconnected client from: {0} was user: {1}", remoteEP, wsClient.UserName); }
                }

                wsClient.ConnectionStatus = ConnectionStatus.Disconnected;
                wsClient.Close(closeReason.ToString());
            }
            catch (Exception exp)
            {
                this.Log(exp, "Exception while session closed from: {0}. Reason: {1}. {2}", remoteEP, closeReason.ToString(), exp.Message);
            }
            finally { Monitor.Exit(this.connectedWebSockets); }
        }

        /// <summary>
        /// Service client message received, Enqueues in service client messaging queue
        /// </summary>
        /// <param name="session">Web socket session</param>
        /// <param name="message">Service client message</param>
        /// <param name="secured">Is secured connection</param>
        private void OnMessageReceived(WebSocketSession session, String message, bool secured)
        {
            if (this.Stopping || !this.Started) return;

            String remoteEP = String.Empty;
            try
            {
                Monitor.Enter(this.connectedWebSockets);

                message = (message ?? String.Empty).Trim();
                remoteEP = session.RemoteEndPoint.ToString();
                WebSocketClients wsClient = null;

                if (this.connectedWebSockets.ContainsKey(remoteEP)) { wsClient = this.connectedWebSockets[remoteEP]; }
                if (wsClient == null)
                {
                    this.Log("Data received from: {0}, but client not found in connected list. Message was: {1}", remoteEP, message);
                    return;
                }

                wsClient.QueueReceivedMessage(message);
            }
            catch (Exception exp) { this.Log(exp, "Exception while data received from client: {0}. {1}. Message was: {2}", remoteEP, message, exp.Message); }
            finally { Monitor.Exit(this.connectedWebSockets); }
        }
      
        #endregion

        #region PRIVATE METHODS

        /// <summary>
        /// Bind simple socket server and secure socket server events 
        /// </summary>
        private void BindServerEvents()
        {
            this.socketServer.NewSessionConnected += ((session) => this.OnNewSession(session, false));
            this.socketServer.SessionClosed += ((session, reason) => this.OnSessionClosed(session, reason, false));
            this.socketServer.NewMessageReceived += ((session, message) => this.OnMessageReceived(session, message, false));

            if (this.runSecureSocketServer)
            {
                this.secureSocketServer.NewSessionConnected += ((session) => this.OnNewSession(session, true));
                this.secureSocketServer.SessionClosed += ((session, reason) => this.OnSessionClosed(session, reason, true));
                this.secureSocketServer.NewMessageReceived += ((session, message) => this.OnMessageReceived(session, message, true));
            }
        }

        #endregion

        #region INTERNAL METHODS

        /// <summary>
        /// Starts web socket server by loading settings from configuration file
        /// </summary>
        /// <param name="runSecureSocketServer">Run secure socket server</param>
        /// <returns>True/False : Socket server started successfully or not</returns>
        internal bool StartSuperWebSocketByConfig(bool runSecureSocketServer)
        {
            this.runSecureSocketServer = runSecureSocketServer;

            var serverConfig = ConfigurationManager.GetSection("socketServer") as SocketServiceConfig;
            if (!SocketServerManager.Initialize(serverConfig)) return false;

            this.socketServer = SocketServerManager.GetServerByName("SuperWebSocket") as WebSocketServer;
            if (this.runSecureSocketServer) { this.secureSocketServer = SocketServerManager.GetServerByName("SecureSuperWebSocket") as WebSocketServer; }

            this.BindServerEvents();

            if (!SocketServerManager.Start()) { SocketServerManager.Stop(); return false; }

            this.Log(
                "Web socket server started successfully. Port: {0}{1}.", this.socketServer.Config.Port,
                (this.runSecureSocketServer ? String.Format(", Secure Port: {0}", this.secureSocketServer.Config.Port) : String.Empty));
            this.Started = true;
            return true;
        }

        /// <summary>
        /// Starts web socket server with the provided settings
        /// </summary>
        /// <param name="serverIP">Server ip</param>
        /// <param name="serverPort">Server port</param>
        /// <param name="secureServerIP">Sercure server ip</param>
        /// <param name="secureServerPort">Sercure server port</param>
        /// <param name="runSecureSocketServer">Run secure socket server</param>
        /// <param name="certificatePath">SSL certificate file path</param>
        /// <returns>True/False : Socket server started successfully or not</returns>
        internal bool StartSuperWebSocketByProgramming(String serverIP, int serverPort, String secureServerIP, int secureServerPort
            , bool runSecureSocketServer, String certificatePath)
        {
            certificatePath = (certificatePath ?? String.Empty).Trim();

            if (certificatePath.Length <= 0) { certificatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "localhost.pfx"); }

            this.runSecureSocketServer = runSecureSocketServer;

            this.socketServer = new WebSocketServer();
            this.socketServer.Setup(
                new RootConfig(), new ServerConfig { Name = "SuperWebSocket", Ip = serverIP, Port = serverPort, Mode = SocketMode.Async }, SocketServerFactory.Instance);

            if (this.runSecureSocketServer)
            {
                this.secureSocketServer = new WebSocketServer();
                this.secureSocketServer.Setup(new RootConfig(), new ServerConfig
                    {
                        Name = "SecureSuperWebSocket",
                        Ip = secureServerIP,
                        Port = secureServerPort,
                        Mode = SocketMode.Sync,
                        Security = "tls",
                        Certificate = new SuperSocket.SocketBase.Config.CertificateConfig
                        {
                            FilePath = certificatePath,
                            Password = "supersocket",
                            IsEnabled = true
                        }
                    }, SocketServerFactory.Instance);
            }

            this.BindServerEvents();

            if (!this.socketServer.Start()) { this.socketServer.Stop(); return false; }
            if (this.runSecureSocketServer)
            {
                if (!this.secureSocketServer.Start()) { this.socketServer.Stop(); this.secureSocketServer.Stop(); return false; }
            }

            this.Log(
                "Web socket server started successfully. Port: {0}{1}.", serverPort,
                (this.runSecureSocketServer ? String.Format(", Secure Port: {0}", secureServerPort) : String.Empty));
            this.Started = true;

            return true;
        }

        /// <summary>
        /// Stops socket server and kills service client sessions
        /// </summary>
        internal void StopSuperWebSocket()
        {
            try
            {
                lock (this.connectedWebSockets)
                {
                    this.Stopping = true;

                    WebSocketClients[] allClients = new WebSocketClients[this.connectedWebSockets.Count];
                    this.connectedWebSockets.Values.CopyTo(allClients, 0);

                    this.connectedWebSockets.Clear();
                    this.authenticatedWebSockets.Clear();

                    foreach (WebSocketClients client in allClients) { try { client.Close("Stopping server"); } catch { } }

                    if (this.socketServer != null) { try { this.socketServer.Stop(); } catch { } }
                    if (this.secureSocketServer != null) { try { this.secureSocketServer.Stop(); } catch { } }

                    this.Started = false;
                }

                this.Log("Web socket server stopped successfully");
            }
            finally { this.Stopping = false; }
        }

        /// <summary>
        /// Checks authenticated client in connected clients collection 
        /// if client exists than it adds in authenticated clients collection
        /// </summary>
        /// <param name="wsClient">Web socket client (service client)</param>
        /// <param name="error">Error</param>
        /// <returns>True/False Client added in authenticated clients collection or not</returns>
        internal bool OnClientAuthenticated(WebSocketClients wsClient, out String error)
        {
            error = String.Empty;
            if (this.Stopping || !this.Started) return false;

            try
            {
                Monitor.Enter(this.connectedWebSockets);

                if (!this.connectedWebSockets.ContainsKey(wsClient.RemoteEndPoint))
                {
                    error = String.Format("Client: {0} not found in connected list", wsClient.RemoteEndPoint);
                    return false;
                }

                if (this.authenticatedWebSockets.ContainsKey(wsClient.UserName))
                {
                    WebSocketClients alreadyAdded = this.authenticatedWebSockets[wsClient.UserName];
                    this.authenticatedWebSockets.Remove(wsClient.UserName);

                    // Closing connection of already connected client
                    alreadyAdded.Close("A client with same name connected from: " + wsClient.RemoteEndPoint);
                }

                this.authenticatedWebSockets.Add(wsClient.UserName, wsClient);

                return true;
            }
            catch (Exception exp)
            {
                error = String.Format("Exception occurred: {0}", exp.Message);
                this.Log(exp, "Exception occurred while client connected. UserName: {0} from {1}. {2}", wsClient.UserName, wsClient.RemoteEndPoint, exp.Message);
            }
            finally { Monitor.Exit(this.connectedWebSockets); }

            return false;
        }

        /// <summary>
        /// Used to identify authenticated service clients count
        /// </summary>
        /// <param name="connectedClientsCount">Authenticated service clients count</param>
        /// <returns>True/False</returns>
        internal bool IsAnyAuthenticatedClient(out int connectedClientsCount) 
        {
            connectedClientsCount = this.authenticatedWebSockets.Count;

            return connectedClientsCount > 0;
        }

        /// <summary>
        /// Recieves color frame data from kinect service and passes 
        /// to authenticated service clients
        /// </summary>
        /// <param name="e">Color frame Event Args</param>
        internal void OnColorFrameReceived(ColorFrameReceivedArgs e)
        {
            String messageString = null;

            foreach (WebSocketClients auth in this.AuthenticatedClients.Values)
            {
                try
                {
                    if (!auth.VideoStreamAllowed) continue;

                    if (messageString == null)
                    {
                        messageString = new ColorImage() { Image = e.Bitmap, KinectDevice = e.KinectDevice, ColorImageFormat = e.Resolution }.Encode();
                    }

                    this.Log("Sending color frame data to client: {0}.", auth.UserName);
                    auth.SendEncodedMessage(messageString);
                }
                catch (Exception exp) { this.Log(exp, "Exception while sending color frame to client: {0}. {1}", auth.UserName, exp.Message); }
            }
        }

        /// <summary>
        /// Recieves depth frame data from kinect service and passes 
        /// to authenticated service clients
        /// </summary>
        /// <param name="e">Depth frame Event Args</param>
        internal void OnDepthFrameReceived(DepthFrameReceivedArgs e)
        {
            String messageString = null;

            foreach (WebSocketClients auth in this.AuthenticatedClients.Values)
            {
                try
                {
                    if (!auth.DepthStreamAllowed) continue;

                    if (messageString == null)
                    {
                        messageString = new DepthImage()
                        {
                            Image = e.Bitmap,
                            KinectDevice = e.KinectDevice,
                            DepthImageFormat = e.Resolution,
                            BytesPerPixel = e.BytesPerPixel,
                            Width = e.Width,
                            Height = e.Height,
                            ImageBitData = e.DepthPixelData
                        }.Encode();
                    }

                    this.Log("Sending depth frame data to client: {0}.", auth.UserName);
                    auth.SendEncodedMessage(messageString);
                }
                catch (Exception exp) { this.Log(exp, "Exception while sending depth frame to client: {0}. {1}", auth.UserName, exp.Message); }
            }
        }

        /// <summary>
        /// Recieves skeleton data from kinect service and passes 
        /// to authenticated service clients
        /// </summary>
        /// <param name="e">Skeleton Event Args</param>
        internal void OnSkeletonFrameReceived(SkeletonFrameReceivedArgs e)
        {
            SkeletonData skeletonData = null;
            foreach (WebSocketClients auth in this.AuthenticatedClients.Values)
            {
                try
                {
                    if (!auth.SkeletonDataAllowed) continue;

                    if (skeletonData == null) { skeletonData = new SkeletonData() { SkeletonFrame = e }; }
                    auth.SendSkeletonData(skeletonData);
                }
                catch (Exception exp) { this.Log(exp, "Exception while sending skeleton data to client: {0}. {1}", auth.UserName, exp.Message); }
            }
        }

        /// <summary>
        /// Returns authenticated service clients list
        /// </summary>
        /// <returns>List of service clients</returns>
        internal List<ServiceClientInfo> GetUsers()
        {
            Hashtable ht = this.AuthenticatedClients;

            List<ServiceClientInfo> sci = new List<ServiceClientInfo>(ht.Count);
            foreach (WebSocketClients client in ht.Values)
            {
                sci.Add(new ServiceClientInfo(ServiceClientType.WebClient, client.UserName, client.UserName
                    , client.ConnectionStatus == ConnectionStatus.Connected));
            }

            return sci;
        }

        #region LOGGING METHODS

        /// <summary>
        /// Writes log
        /// </summary>
        /// <param name="logFormat">Log format</param>
        /// <param name="args">Arguments</param>
        internal void Log(String logFormat, params Object[] args) { this.Log(null, logFormat, args); }
       
        /// <summary>
        /// Writes log
        /// </summary>
        /// <param name="exp">Exception</param>
        /// <param name="logFormat">Log format</param>
        /// <param name="args">Arguments</param>
        internal void Log(Exception exp, String logFormat, params Object[] args)
        {
            Logger.Log(exp, "[WebSocketServer] >> {0}", String.Format(logFormat, args));
        }

        #endregion

        #endregion
    }
}